package com.espirit.eap.util;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.net.URLConnection;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.ClassUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.poi.hssf.usermodel.HSSFClientAnchor;
import org.apache.poi.hssf.usermodel.HSSFDateUtil;
import org.apache.poi.hssf.usermodel.HSSFPicture;
import org.apache.poi.hssf.usermodel.HSSFShape;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.Drawing;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFAnchor;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFShape;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.struts2.ServletActionContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.espirit.common.EncryptedFile;
import com.espirit.common.TxFile;
import com.espirit.eap.annotation.Dict;
import com.espirit.eap.manager.DictionaryService;
import com.espirit.eap.manager.EapImport;
import com.espirit.eap.manager.I18n;
import com.espirit.eap.manager.I18nService;
import com.espirit.eap.pagelayout.ModelService;
import com.espirit.eap.sa.LoginUser;
import com.googlecode.cswish.Config;
import com.googlecode.cswish.annotation.GenericType;
import com.googlecode.cswish.annotation.ParamScope;
import com.googlecode.cswish.annotation.Protected;
import com.googlecode.cswish.annotation.Rest;
import com.googlecode.cswish.exception.ApplicationException;
import com.googlecode.cswish.model.ModelInfo;
import com.googlecode.cswish.model.PageElement;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.model.PropertyInfo;
import com.googlecode.cswish.model.Reference;
import com.googlecode.cswish.oosearch.Condition;
import com.googlecode.cswish.oosearch.QLInfo;
import com.googlecode.cswish.security.auth.UserInfo;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.CacheService;
import com.googlecode.cswish.struts.spring.CacheService.Wrapper;
import com.googlecode.cswish.struts.spring.Executor;
import com.googlecode.cswish.struts.spring.GenericJPAService;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.StringUtil;
import com.googlecode.cswish.util.bean.ModelUtil;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.util.reflection.ReflectionContextState;

import ognl.OgnlContext;

/**
 * 扩展通用服务, 增强如下功能:
 * 
 * 1. 文件的导入导出
 * 2. 文件上传与查看
 * 3. 打印
 * 
 * @author feng Date: 2009-5-16
 */
@Service
public class StatGenericJPAService extends GenericJPAService {

	private static final Log logger = LogFactory.getLog(StatGenericJPAService.class);
	
	@Resource
	private BeanIntrospector beanIntrospector;
	
	@Resource
	private Config config;
	
	@Resource
	protected Routing routing;
	
	@Resource
	protected Functions functions;
	
	@Resource
	private CacheService cacheService;
	
	//@Resource
	//protected JavaScriptShell javaScriptShell;
	
	private Condition searchCondition = new Condition();
	
	private Wrapper<Executor<Object, Object[]>> modelMapping;
	
	@PostConstruct
	private void init() {
		searchCondition.setCompareFlag(Condition.COMPARE_AUTO_LIKEFIRST);
		Wrapper wrapper = cacheService.newVar(Executor.class);
		modelMapping = wrapper;
	}
	
	private I18nService i18nService;
	
	private ModelService modelService;
	
	private TxFileService txFileService;
	
	private I18nService getI18nService() {
		if (i18nService == null) {
			i18nService = objectFactory.getBean(I18nService.class, false);
		}
		return i18nService;
	}
	
	private ModelService getModelService() {
		if (modelService == null) {
			modelService = objectFactory.getBean(ModelService.class, false);
		}
		return modelService;
	}
	
	private TxFileService getTxFileService() {
		if (txFileService == null) {
			txFileService = objectFactory.getBean(TxFileService.class, false);
		}
		return txFileService;
	}

	public void registerModeMapping(Executor<Object, Object[]> modelMapping) {
		this.modelMapping.set(modelMapping);
	}
	
	/*
	@Protected
	@Transactional
	public <T> T add(@GenericType T model) {
    	if (load(model) != null) {
    		throw new ApplicationException("errors.duplicate", getId(model));
    	}
    	
    	if (model != null) {
			// update the reference
    		String simpleModelName = beanIntrospector.getSimpleName(ClassUtil.getClassName(model));
    		ModelUtil.updateParentChildRelationship(model, new I18nInterceptor(simpleModelName));
    		model = merge(model);
    	}
    	return model;
    }
	
	@Override
	@Transactional
    @Rest(type="_update")
	public Object update(@GenericType Object model) {
		String simpleModelName = beanIntrospector.getSimpleName(ClassUtil.getClassName(model));
        return update(model, false, getEnableFields(false, false), new I18nInterceptor(simpleModelName));
    }

	public <T> PageInfo<T> searchTip(String query, int start, int limit, String sort, String dir) {
		// search all the keywords which starts with 'keywords'
		String ql = " from " + SearchWords.class.getName() + " a where a.keywords like ?";
		QLInfo qlInfo = new QLInfo(ql, false, query + '%');
		// search the default sort field
		if (sort == null) {
			sort = "searchCount";
		}
		qlInfo.setFirstResult(start);
		qlInfo.setMaxResults(limit);
		qlParse.setSort(qlInfo, SearchWords.class.getName(), sort);
		qlInfo.setDir(dir);
		return super.searchByQl(qlInfo, true);
	}*/
	
	protected UserInfo getLoginUserInfo(String userId) {
		LoginUser user  = load(LoginUser.class, new Integer(userId));
		return user;
	}
	
	public void export(Reference<QLInfo> qlInfoRef, String method, List<Export> results) {
		QLInfo qlInfo = qlInfoRef.get();
		// export all result
		qlInfo.setFirstResult(0);
		qlInfo.setMaxResults(0);
		List data = super.searchByQl(qlInfo, false).getData();
		List exportData = new ArrayList();
		if (results != null && !results.isEmpty()){
			for(Export export : results){
				if (export == null || export.getId() == null) continue;
				for(Object obj : data){
					Integer id = (Integer) ModelUtil.getProperty(obj, "id");
					if (export.getId().equals(id)){
						exportData.add(obj);
					}
				}
			}
			data = exportData;
		}
		String modelClassName = ClassUtil.getRuntimeType(GenericType.MODEL);
		I18n i18n = getI18nService().getI18n(getModelService().getModel(modelClassName).getI18n());
		String modelI18nName;
		if (i18n == null) {
			modelI18nName = modelClassName.substring(modelClassName.lastIndexOf('.') + 1);
		} else {
			modelI18nName = i18n.getName();
		}
		
		if (StringUtils.isEmpty(method)) {
			method = "_search";
		}
		Map<String, PageElement> fields = getExportFields(modelClassName, method);
		export(data, fields, modelI18nName);
	}
	
	public Map<String , PageElement> getExportFields(String modelClassName , String method){
		PageElements ps = getPageElementService().getPageElements(modelClassName, method);
		Map<String, PageElement> modelFields = ((PageElements)ps.findElementByType("pageinfo")).getAllElements();
		Map<String, PageElement> fields = new LinkedHashMap<String, PageElement>();
		for (PageElement pageElement : modelFields.values()) {
			// only choose the visible list element 
			if ( (pageElement.getHidden() == null || !pageElement.getHidden()) && pageElement.getName().startsWith("execute.")) {
				if (pageElement.getName().equals("execute.currencyPrice")) {
					// special logic for the currency price, show this field by 2 fields
					PageElement priceElement = new PageElement();
					priceElement.setName("execute.price");
					priceElement.setLabel("model.espirit.sale.rfqItem.price");
					fields.put(priceElement.getName(), priceElement);
					
					PageElement currencyCodeElement = new PageElement();
					currencyCodeElement.setName("execute.currency.name");
					currencyCodeElement.setLabel("model.espirit.eap.manager.currency");
					fields.put(currencyCodeElement.getName(), currencyCodeElement);
				} else {
					fields.put(pageElement.getName(), pageElement);
					
					// labelProperty
					String modelElementName = "model." + pageElement.getName().substring(8);
					PageElement modelPageElement = ps.getElement(modelElementName);
					if (modelPageElement == null){
						modelElementName = "execute." + pageElement.getName().substring(8);
						modelPageElement = ps.getElement(modelElementName);
					}
					if (modelPageElement != null) {
						String labelProperty = (String) modelPageElement.getExtraParam("labelProperty");
						if (labelProperty != null) {
							pageElement.addExtraParams("labelProperty", labelProperty);
						}
					}
				}
			}
		}
		fields.remove("execute._selectButton");
		return fields;
	}
	
	/**
	 * Export the searched result, TODO: Support the permission control
	 * 
	 * @param qlInfoRef
	 */
	@SuppressWarnings("unchecked")
	@Protected
	public Result export(List data, Map<String, PageElement> dataFields, String excelFilName) {
		try {
			HttpServletResponse response = ServletActionContext.getResponse();
	        String fileName = new String(excelFilName.getBytes("UTF-8"), "ISO8859_1")  + ".xls";
	        response.reset();
	        String contentType = getContentTypeFor(fileName);
	        response.setContentType(contentType);
	        response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
			
			OutputStream outputStream = response.getOutputStream();
			Workbook book = new HSSFWorkbook();
			Sheet sheet = book.createSheet(excelFilName);
			
			// Output the title & width
			int column = 0;
			Locale locale = ActionContext.getContext().getLocale();
			List<String> listModelFields = new ArrayList<String>(dataFields.size());
			Row row = sheet.createRow(0);
			for (PageElement pageElement : dataFields.values()) {
				listModelFields.add(pageElement.getName());
				String key = pageElement.getLabel();
				I18n i18n = getI18nService().getI18n(key, false);
				String localI18nName = i18n == null ? key : i18n.getNameByLocale(locale);
				Cell cell = row.createCell(column++);
				cell.setCellValue(localI18nName);
			}
			
			// adjust output property
			for (int i = 0, len = listModelFields.size(); i < len; i++) {
				String fieldName = listModelFields.get(i);
				PageElement pageElement = dataFields.get(fieldName);
				if  ("execute.partNumber".equals(fieldName) || "execute.brandName".equals(fieldName)){
					continue;
				}
					
				boolean isDropDown = ("modelselect".equals(pageElement.getType())
						|| "combobox".equals(pageElement.getType())) ;
				
				if (isDropDown) {
					String displayProperty = (String) pageElement.getExtraParam("labelProperty");
					if (displayProperty == null) {
						// default property is 'name', see TagUtil.getComboboxDisplayName
						displayProperty = "name"; 
					}

					String fullDisplayProperty;
					if (fieldName.endsWith(".id")) {
						fullDisplayProperty = fieldName.substring(0, fieldName.length() - 2) + displayProperty;
					} else {
						fullDisplayProperty = fieldName + '.' + displayProperty;
					}

					if (fullDisplayProperty.startsWith("execute.") || fullDisplayProperty.startsWith("model.")) {
						listModelFields.set(i, fullDisplayProperty);
					} else {
						listModelFields.set(i, "model." + displayProperty);
					}
				}
			}

			// Output the data
			Map context = new HashMap();
			int rowIndex = 1;
			List<String> propertyNames = getPropertyNames(listModelFields);
			
			boolean [] intProperties = new boolean[propertyNames.size()];
			if (data.size() > 0) {
				Object model = data.get(0);
				int i = 0;
				for (String propertyName : propertyNames) {
					String propertyType = beanIntrospector.getPropertyType(model.getClass().getName(), propertyName);
					boolean isIntType;
					if (propertyType != null) {						
						isIntType = propertyType.equals("java.lang.Long") || propertyType.equals("java.lang.Integer") || propertyType.equals("java.math.BigDecimal");
					} else {
						isIntType = false;
					}
					intProperties[i++] = isIntType;
				}
			}

			String modelClassName = ClassUtil.getRuntimeType(GenericType.MODEL);
			ModelInfo mi = beanIntrospector.getModelInfo(modelClassName);
			DictionaryService dictionaryService = objectFactory.getBean(DictionaryService.class, true);
			for (Object model : data) {
				column = -1;
				row = sheet.createRow(rowIndex++);
				for (String propertyName : propertyNames) {
					column++;
					Object v = functions.val(model, propertyName, propertyName);
					//Object v = ognlUtil.getValue(propertyName, context, model);
					Cell cell = row.createCell(column);
					
					if (intProperties[column]){
						PropertyInfo pi = mi.getProperty(propertyName);
						boolean setted = false;
						if (pi != null){
							Method method = pi.getReadMethod();
							if (method != null){
								Dict dict = method.getAnnotation(Dict.class);
								if (dict != null){
									String type = dict.type();
									String value = dictionaryService.getNameByTypeValue(type, v);
									
									cell.setCellValue(value);
									cell.setCellType(Cell.CELL_TYPE_STRING);
									setted = true;
								}
							} 
						} 
						if (!setted){
							if (v == null){
								v = "0";
							}
							cell.setCellValue(Double.valueOf(v.toString()));
							cell.setCellType(Cell.CELL_TYPE_NUMERIC);
						}
					} else {
						String value;
						if (v instanceof Date) {
							boolean isTime = propertyName != null && (propertyName.endsWith("Time") || propertyName.endsWith("time"));
							String format = "yyyy-MM-dd";
							if (isTime){
								format = "yyyy-MM-dd HH:mm";
							} 
							value = new SimpleDateFormat(format).format(v);
						} else {
							value = v == null ? "" : v.toString();
						}
						cell.setCellValue(value);
						cell.setCellType(Cell.CELL_TYPE_STRING);
					}
				}
			}

			// add defined all cell above to case.
			book.write(outputStream);
		} catch (Exception ex) {
			logger.error("Fail to generate excel", ex);
		}
		return null;
	}
	
	@Protected
	public void exportToFile(List data, Map<String, PageElement> dataFields, String excelFilName) {
		try {
			excelFilName = new String(excelFilName.getBytes("UTF-8"), "ISO8859_1")  + ".xls";
			Workbook book = new HSSFWorkbook();
			Sheet sheet = book.createSheet("sheet");
			// Output the title & width
			int column = 0;
			Locale locale = ActionContext.getContext().getLocale();
			List<String> listModelFields = new ArrayList<String>(dataFields.size());
			Row row = sheet.createRow(0);
			for (PageElement pageElement : dataFields.values()) {
				listModelFields.add(pageElement.getName());
				String key = pageElement.getLabel();
				I18n i18n = getI18nService().getI18n(key, false);
				String localI18nName = i18n == null ? key : i18n.getNameByLocale(locale);
				Cell cell = row.createCell(column++);
				cell.setCellValue(localI18nName);
			}
			
			// adjust output property
			for (int i = 0, len = listModelFields.size(); i < len; i++) {
				String fieldName = listModelFields.get(i);
				PageElement pageElement = dataFields.get(fieldName);
				boolean isDropDown = "modelselect".equals(pageElement.getType())
						|| "combobox".equals(pageElement.getType());
				if (isDropDown) {
					String displayProperty = (String) pageElement.getExtraParam("labelProperty");
					if (displayProperty == null) {
						// default property is 'name', see TagUtil.getComboboxDisplayName
						if (fieldName.endsWith(".id")) {
							displayProperty = fieldName.substring(0, fieldName.length() - 2) + "name";
						} else {
							displayProperty = fieldName;
						}
					}
					if (displayProperty.startsWith("execute.") || displayProperty.startsWith("model.")) {
						listModelFields.set(i, displayProperty);
					} else {
						listModelFields.set(i, "model." + displayProperty);
					}
				}
			}

			// Output the data
			Map context = new HashMap();
			int rowIndex = 1;
			List<String> propertyNames = getPropertyNames(listModelFields);
			
			boolean [] intProperties = new boolean[propertyNames.size()];
			if (data.size() > 0) {
				Object model = data.get(0);
				int i = 0;
				for (String propertyName : propertyNames) {
					String propertyType = beanIntrospector.getPropertyType(model.getClass().getName(), propertyName);
					boolean isIntType;
					if (propertyType != null) {						
						isIntType = propertyType.equals("java.lang.Long") || propertyType.equals("java.lang.Integer") || propertyType.equals("java.math.BigDecimal");
					} else {
						isIntType = false;
					}
					intProperties[i++] = isIntType;
				}
			}

			
			for (Object model : data) {
				column = -1;
				row = sheet.createRow(rowIndex++);
				for (String propertyName : propertyNames) {
					column++;
					Object v = functions.val(model, propertyName, propertyName);
					//Object v = ognlUtil.getValue(propertyName, context, model);
					Cell cell = row.createCell(column);
					
					if (intProperties[column]){
						if (v == null){
							v = "0";
						}
						cell.setCellValue(Double.valueOf(v.toString()));
						cell.setCellType(Cell.CELL_TYPE_NUMERIC);
					} else {
						String value;
						if (v instanceof Date) {
							value = new SimpleDateFormat("yyyy-MM-dd").format(v);
						} else {
							value = v == null ? "" : v.toString();
						}
						cell.setCellValue(value);
						cell.setCellType(Cell.CELL_TYPE_STRING);
					}
				}
			}

			// add defined all cell above to case.
			FileOutputStream to = new FileOutputStream(excelFilName);
			book.write(to);;
		} catch (Exception ex) {
			logger.error("Fail to generate excel", ex);
		}
	}
	
	// skip the first node, such as 'execute.' or 'model.'
	private List<String> getPropertyNames(List<String> modelFields) {
		List<String> names = new ArrayList<String>(modelFields.size());
		for (String name : modelFields) {
			int index = name.indexOf('.');
			if (index > 0) {
				names.add(name.substring(index + 1));
			} else {
				names.add(name);
			}
		}
		return names;
	}

	private Map<String, PropertyInfo> getPropertyType(Class clazz,  List<String> propertyNames) {
		ModelInfo rootModelInfo = beanIntrospector.getModelInfo(clazz.getName());
		
		Map<String, PropertyInfo> typeMapping = new HashMap<String, PropertyInfo>();
		for (String propertyName : propertyNames) {
			if (StringUtils.isEmpty(propertyName)) continue;
			String[] names = StringUtils.split(propertyName, '.');
			int i = 0;
			PropertyInfo propertyInfo;
			ModelInfo modelInfo = rootModelInfo;
			for (int len = names.length - 1; i < len; i++) {
				String name = names[i];
				propertyInfo = modelInfo.getProperty(name);
				modelInfo = beanIntrospector.getModelInfo(propertyInfo.getType());
			}
			propertyInfo = modelInfo.getProperty(names[i]);
			if (propertyInfo != null){
				typeMapping.put(propertyName, propertyInfo);
			}
		}
		return typeMapping;
	}
	
	@Protected
	@Transactional
	public List<Object> convertFileToModelList(EapImport model) {
		return convertFileToModelList(model, null);
	}
	
	@Protected
	@Transactional
	public List<Object> convertFileToModelList(EapImport model , Class clazz) {
		int row = 1;
		return convertFileToModelList(model, clazz, row);
	}
	
	@Protected
	@Transactional
	public List<Object> convertFileToModelList(EapImport model , Class clazz, Integer row) {
//		int row = 1;
		int column = 0;
		FileInputStream input = null;
		try {
			File file = model.getFile();
			
			input = new FileInputStream(file);
			Workbook book;
			if (model.getFileFileName().endsWith("xls")) {
				book = new HSSFWorkbook(new POIFSFileSystem(input));
			} else {
				book = new XSSFWorkbook(input);
			}
			
			// get a Sheet object.
			Sheet sheet = book.getSheetAt(0);
			int rowCount = sheet.getPhysicalNumberOfRows();
			
			String modelName = null;
			if (clazz == null){
				modelName = ClassUtil.getRuntimeType(GenericType.MODEL);
			} else {
				modelName = clazz.getName();
			}
			PageElements ps = getPageElementService().getPageElements(modelName, "analyseImportFileColumns");
			PageElement pageElement = ps.findElementByType("importsetting");
			Collection pageDefImportPropertyNames = pageElement == null ? null : (Collection)pageElement.getExtraParam("targetModelProperties");
			// the property name list which the model needs
			List<String> propertyNames;
			if (pageDefImportPropertyNames == null || pageDefImportPropertyNames.isEmpty()) {
				propertyNames = model.getImportPropertyNames();
			} else {
				propertyNames = new ArrayList(pageDefImportPropertyNames);
			}
			
			// prepare data
			Map<String, Map<Object, Object>> cache = new HashMap<String, Map<Object,Object>>();
			Object updateMOdel = null;
			if (clazz == null){
				updateMOdel = objectFactory.buildBean(GenericType.MODEL, Object.class, null);
				clazz = updateMOdel.getClass();
			} else {
				updateMOdel = objectFactory.buildBean(modelName, Object.class, null);
			}
			Map<String, PropertyInfo> typeMapping = getPropertyType(clazz, propertyNames);
			
			int[] mappingColumns = model.getMappingColumns();
			
			// read model data from excel (the order is by column)
			// use Object[] to store the model data, 
			Object[][] data = new Object[rowCount][propertyNames.size()];
			DictionaryService dictionaryService = objectFactory.getBean(DictionaryService.class, true);
			for (; row < rowCount; row++) {
				column = 0;
				Row sheetRow = sheet.getRow(row);
				if (sheetRow == null) continue;
				for (String propertyName : propertyNames) {
					// get 1st-Column,1st-Row content.
					int mappingColumn = column;
					if (mappingColumns != null && column < mappingColumns.length) {
						mappingColumn = mappingColumns[column];
					}
					Cell cell = null;
					if (mappingColumn != -1) {
						cell = sheetRow.getCell(mappingColumn);
					}
					
					if (cell != null) {
						boolean isDate = false;
						String dictType = "";
						PropertyInfo propertyInfo = typeMapping.get(propertyName);
						if (propertyInfo != null) {
							isDate = Date.class.getName().equals(propertyInfo.getType());
							if (propertyInfo.getType().equals("java.lang.Integer")){
								Method method = propertyInfo.getReadMethod();
								if (method != null){
									Dict dict = method.getAnnotation(Dict.class);
									if (dict != null){
										dictType = dict.type();
									}
								} 
							}
						}
						
						Object value;
						if (isDate && cell.getCellType() == Cell.CELL_TYPE_NUMERIC) {
							value = cell.getDateCellValue();
						} else {
							value = getCellValue(cell);
						}
						
						String strValue;
						if (value instanceof String || value instanceof Number) {
							strValue = (String)ognlUtil.getXWorkConverter().convertValue(value, String.class);
						} else {
							strValue = null;
						}
						if (strValue != null && !strValue.equals("")) {
							Executor<Object, Object[]> modelMapping = this.modelMapping.get();
							if (modelMapping != null) {
								Object mappingValue = modelMapping.execute(new Object[] {model, cache, propertyName, value});
								if (mappingValue != null) {
									value = mappingValue;
								}
							}
							
							if (value != null) {
								if (propertyInfo != null && value instanceof String) {
									int length = propertyInfo.getLength();
									if (length == 0) {
										length = 255;
									}
									String str = (String)value;
									if (str.length() > length) {
										throw new ApplicationException("errors.excelRowColumn.toolong", row + 1, column + 1);
									}
								}
								
								// special conversion logic for the number to string
								if (value instanceof Number && propertyInfo != null 
										&& String.class.getName().equals(propertyInfo.getType())) {
									value = strValue;
								}
								
								if (!dictType.equals("")){
									value = dictionaryService.getValueByTypeName(dictType, value.toString());
								}
								data[row][column] = value;
							}
						} else {
							data[row][column] = value;
						}
					}
					column++;
				}
			}
			
			List<Object> updateModels = new ArrayList<Object>(rowCount);
			OgnlContext context1 = (OgnlContext) ActionContext.getContext().getContextMap();
			OgnlContext context = new OgnlContext(context1.getClassResolver(), context1.getTypeConverter(), context1.getMemberAccess());
			ReflectionContextState.setCreatingNullObjects(context, true);
			
			int len = propertyNames.size();
			for (row = 1; row < rowCount; row++) {
				boolean hasRowValue = false;
				Object theModel = clazz.newInstance();
				for (column = 0; column < len; column++) {
					Object value = data[row][column];
					String propertyName = propertyNames.get(column);
					
					boolean hasValue = false;
					if (value instanceof String) {
						hasValue = ((String)value).length() > 0;
					} else {
						hasValue = value != null;
					}
					if (hasValue) {
						hasRowValue = true;
						ModelUtil.setProperty(theModel, propertyName, value);
					}
				}
				if (hasRowValue) {
					updateModels.add(theModel);
				}
			}
			return updateModels;
		} catch (Exception ex) {
			logger.error("Fail to import excel file", ex);
			
			if (ex instanceof ApplicationException) {
				throw (ApplicationException)ex;
			} else {
				throw new ApplicationException("errors.excelRowColumn", row, column);
			}
		} finally {  
            try {  
                if (input != null) {  
                    input.close();  
                }
            } catch (IOException ex) { 
            	logger.error("Fail to close excel file", ex);
            }  
        }  
	}

	private Object getCellValue(Cell cell) {
		if (cell == null) {
			return "";
		}
		
		Object value;
		switch (cell.getCellType()) {
		case Cell.CELL_TYPE_STRING:
			value = cell.getStringCellValue().trim();
			break;
		case Cell.CELL_TYPE_NUMERIC:
			if (HSSFDateUtil.isCellDateFormatted(cell)) {  
				value = cell.getDateCellValue();
			} else {
				value = cell.getNumericCellValue();
			}
			break;
		case Cell.CELL_TYPE_BOOLEAN:
			value = cell.getBooleanCellValue();
			break;
		case Cell.CELL_TYPE_BLANK:
			value = "";
			break;
		default:
			value = "";
			break;
		}
		return value;
	}
	private void setCellValue(Cell cell, Object val) {
		if (val == null) {
			cell.setCellValue("");
		} else if (val instanceof String) {
			cell.setCellValue((String)val);
		} else if (val instanceof Integer) {
			cell.setCellValue((Integer)val);
		} else if (val instanceof Date) {
			cell.setCellValue((Date)val);
		} else if (val instanceof Double) {
			cell.setCellValue((Double)val);
		}
	}
	
	/**
	 * Import the model data from excel file
	 * 
	 * @param file
	 * @param ps
	 */
	public List<Object> importFile(EapImport model) {
		return convertFileToModelList(model);
	}
	
	/**
	 * 
	 * @param model 
	 * @param importPropertyNames the defined model columns
	 * @param excelColumns the column name from excel
	 * @return
	 */
//	@Rest(type="importFile")
	@Rest(simpleServiceName="loginUser", type="free")
	public void analyseImportFileColumns(EapImport model, List<String> importPropertyNames,
			List<String> excelColumns, @ParamScope(visible=false, value="#ps")PageElements ps) {
		FileInputStream input = null;
		try {
			File file = model.getFile();
			
			input = new FileInputStream(file);
			Workbook book;
			if (model.getFileFileName().endsWith("xls")) {
				book = new HSSFWorkbook(new POIFSFileSystem(input));
			} else {
				book = new XSSFWorkbook(input);
			}
			
			Sheet sheet = book.getSheetAt(0);
			int rowCount = sheet.getPhysicalNumberOfRows();
			Row sheetRow = sheet.getRow(0);
			
			for (int i = 0; i < 100; i++) {
				Cell cell = sheetRow.getCell(i);
				if (cell != null) {
					Object value = getCellValue(cell);
					if (value != null) {
						excelColumns.add(value.toString());
					} else {
						excelColumns.add("");
					}
				} else {
					break;
				}
			}
			
			// page definition has the higher priority
			PageElement pageElement = ps.findElementByType("importsetting");
			Collection pageDefImportPropertyNames = pageElement == null ? null : (Collection)pageElement.getExtraParam("targetModelProperties");
			if (pageDefImportPropertyNames == null || pageDefImportPropertyNames.isEmpty()) {
				String realModelName = ClassUtil.getRuntimeType(GenericType.MODEL);
				try {
					Class clazz = ClassUtils.getClass(realModelName + "Import");
					EapImport realImportModel = (EapImport) clazz.newInstance();
					importPropertyNames.addAll(realImportModel.getImportPropertyNames());
				} catch (Exception ex) {
					logger.error("Fail to get the import model column, use the default columns", ex);
					importPropertyNames.addAll(model.getImportPropertyNames());
				}
			} else {
				importPropertyNames.addAll(pageDefImportPropertyNames);
			}
			
			// add blank column if input file hasn't enough column
			for (int i = excelColumns.size(), len = importPropertyNames.size(); i < len; i++) {
				excelColumns.add("");
			}
			for (int i = importPropertyNames.size(); i < excelColumns.size(); i++) {
				importPropertyNames.add("");
			}
		} catch (Exception ex) {
			logger.error("Fail to analysis the excel file", ex);
		} finally {  
            try {  
                if (input != null) {  
                    input.close();  
                }
            } catch (IOException ex) { 
            	logger.error("Fail to close excel file", ex);
            }  
        } 
	}
	
	/*
	public <T> Result report(@GenericType T model, List<?> data) {
		try {
			String className = model == null ? ClassUtil.getRuntimeType(GenericType.MODEL)
					: model.getClass().getName();
			String simpleName = beanIntrospector.getSimpleName(className).replace('.', '/');
			String jasperFile = "/report/" + simpleName + ".jasper";
			ActionContext ctx = ActionContext.getContext();
			Locale locale = ctx.getLocale();
			String language = locale == null ? "zh" : locale.getLanguage();
			if (language.equals("en")){
				jasperFile = "/report/" + simpleName + "_en.jasper";
			}
			
			Map<String, Object> parameters = new HashMap<String, Object>();
			InputStream inputStream = this.getClass().getResourceAsStream(jasperFile);
			parameters.put("model", model);
			Company company = getMyCompany();
			parameters.put("company", company);
			ReportSetting reportSetting = getReportSetting();
			parameters.put("reportSetting", reportSetting);
			
			JRDataSource dataSource;
			if (data == null || data.size() == 0) {
				dataSource = new JREmptyDataSource();
			} else {
				dataSource = new JRBeanCollectionDataSource(data);
			}
			JasperPrint print = JasperFillManager.fillReport(inputStream, parameters, dataSource);
			byte[] reportContext = new byte[0];
			
			boolean isPdf =(reportSetting.getReportFileType() == null || reportSetting.getReportFileType().getName().equals(ReportFileType.PDF));
			if (isPdf) {
				reportContext = JasperExportManager.exportReportToPdf(print);
			} else {
				JRXlsExporter exporter = new JRXlsExporter();  
				ByteArrayOutputStream oStream = new ByteArrayOutputStream();  
				exporter.setParameter(JRExporterParameter.JASPER_PRINT, print);  
				exporter.setParameter(JRExporterParameter.OUTPUT_STREAM, oStream);  
				exporter.setParameter(JRXlsExporterParameter.IS_REMOVE_EMPTY_SPACE_BETWEEN_ROWS, Boolean.TRUE);  
				exporter.setParameter(JRXlsExporterParameter.IS_REMOVE_EMPTY_SPACE_BETWEEN_COLUMNS, Boolean.TRUE);
				exporter.setParameter(JRXlsExporterParameter.IS_ONE_PAGE_PER_SHEET, Boolean.FALSE);  
				exporter.setParameter(JRXlsExporterParameter.IS_WHITE_PAGE_BACKGROUND, Boolean.FALSE);  
				exporter.exportReport();
				reportContext = oStream.toByteArray(); 
			} 
			
			String simpleClassName = ModelInfo.getStaticClassName(model.getClass().getSimpleName());
			if (model instanceof Event){
				simpleClassName += "_"+((Event)model).getOrderNbr();
	        }
			outputFile(simpleClassName, isPdf, reportContext);
		} catch (Exception ex) {
			logger.error("generate report", ex);
		}
		return null;
	}*/
	
	private void outputFile(String fileName, boolean isPdf, byte[] context) throws IOException {
		// TODO: use FileUtil.export
		
		HttpServletResponse response = ServletActionContext.getResponse();
		
        String encodedFileName = new String(fileName.getBytes("UTF-8"), "ISO8859_1");
        response.reset();
        
        if (isPdf) {
        	response.setContentType("bin");
        	encodedFileName += ".pdf";
        } else {
        	boolean isExcelX = fileName.endsWith(".xlsx");
        	boolean isExcel = fileName.endsWith(".xls");
        	if (!isExcelX && !isExcel) {
        		encodedFileName += ".xls";
        	}
        	
        	String contentType = getContentTypeFor(encodedFileName);
        	response.setContentType(contentType);
        }
        response.addHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
		
		OutputStream outputStream = response.getOutputStream();
		outputStream.write(context, 0, context.length);
	}
	
	private String getContentTypeFor(String fileName) {
		String contentType = URLConnection.getFileNameMap().getContentTypeFor(fileName);
		if (contentType == null) {
			boolean isExcel = fileName.endsWith(".xls") || fileName.endsWith(".xlsx");
			if (isExcel) {
				contentType = "application/vnd.ms-excel";
        	} else {
        		boolean isPdf = fileName.endsWith(".pdf");
        		if (isPdf) {
        			contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        		}
        	}
		}
		return contentType;
	}
	
	@Rest(type="report")
	public Object print(@GenericType Object model, String path) {
		return view(model);
	}
	
	
	/**
	 * 按模板输出新的excel到response
	 * <p>
	 * 模板所支持的excel表达式格式：<br>
	 * 1) Freemarker宏: 以<开始,以>结束,目前只支持list, 比如: <#list collectionVar as itemVar><br>
	 * 2) Freemarker表达式: 每个cell都可以有多个表达式， 比如: ${xxx}aaa${yyy}<br>
	 * <p>
	 * 模板所支持的图片：<br>
	 * 1) 所有的图片依次被model中picture_xxx的图片变量所替换
	 * <p>
	 * @param model the template variable
	 * @param tplFilePath the input template file path
	 * @param outputFileName the output file name
	 * @return
	 */
	@Rest(type="report")
	public Result printExcel(@GenericType Object model, String tplFilePath ,String outputFileName) {
		Workbook book = prepareExcelData(model, tplFilePath);
		try {
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			book.write(baos);
	//		String fileName = tplFilePath.substring(tplFilePath.lastIndexOf('/') + 1);
			byte[] reportContext = baos.toByteArray();
	        outputFile(outputFileName, false, reportContext);
		
		} catch (Exception ex) {
			logger.error("Fail to analysis the excel file", ex);
		} 
		return null;
	}
	
	public void exportExcel(@GenericType Object model, String tplFilePath ,String outputFileName){
		Workbook book = prepareExcelData(model, tplFilePath);
		try {
			OutputStream out = new FileOutputStream(outputFileName);
			book.write(out);
			out.flush();
			out.close();
		} catch (Exception ex) {
			logger.error("Fail to analysis the excel file", ex);
		}
	}
	
	private Workbook prepareExcelData(@GenericType Object model, String tplFilePath){
		FileInputStream input = null;
		try {
			File file = new File(tplFilePath);
			
			input = new FileInputStream(file);
			Workbook book;
			if (tplFilePath.endsWith("xls")) {
				book = new HSSFWorkbook(new POIFSFileSystem(input));
			} else {
				book = new XSSFWorkbook(input);
			}
			
			Sheet sheet = book.getSheetAt(0);
			int rowCount = sheet.getPhysicalNumberOfRows();
			int physicalNumber = 0;
			// use physicalNumber, because the macro can change the row count, 9999 is used for the safe check because of any accident
			for (int rowIndex = 0; physicalNumber < rowCount && rowIndex < 9999; rowIndex++) {
				Row sheetRow = sheet.getRow(rowIndex);
				if (sheetRow == null) {
					continue;
				}
				physicalNumber++;
				
				for (int cellIndex = sheetRow.getFirstCellNum(), max = sheetRow.getLastCellNum(); cellIndex <= max; cellIndex++) {
					Cell cell = sheetRow.getCell(cellIndex);
					if (cell != null) {
						if (cell.getCellType() == Cell.CELL_TYPE_STRING) {
							String val = cell.getStringCellValue();
							if (val.length() > 0) {
								int endFlag = val.indexOf('>', 2);
								if (val.startsWith("<#") && endFlag > 0) {
									// active macro
									String [] macros = StringUtils.split(val.substring(2, endFlag), ' ');
									rowIndex = runMacro(macros, model, book, sheet, rowIndex, cellIndex);
									cellIndex = max;
								} else {
									tryToReplaceVar(model, cell, val);
								}
							}
						}
					}
				}
			}

			replacePicture(book, sheet, model);
			
			return book;
		} catch (Exception ex) {
			logger.error("Fail to analysis the excel file", ex);
		} finally {  
            try {  
                if (input != null) {  
                    input.close();  
                }
            } catch (IOException ex) { 
            	logger.error("Fail to close excel file", ex);
            } 
        }
		return null;
	}

	private int runMacro(String[] macros, Object model, Workbook book, Sheet sheet, int rowIndex, int cellIndex) {
		String macroName = macros[0];
		if ("list".equals(macroName) || macroName.startsWith("listr")) {
			if (macros.length == 4) {
				boolean isInsert = macroName.length() == 4;
				int replaceRowCount = 0;
				if (!isInsert) {
					String count = macroName.substring(5);
					replaceRowCount = count.length() == 0 ? 0 : Integer.parseInt(count);
				}
				return runMacroList(macros[1], macros[3], model, sheet, rowIndex, cellIndex, replaceRowCount);
			} else {
				if (logger.isErrorEnabled()) { 
					logger.error("IGNORE, Wrong macro format found: " + macros + ", please use <#list collectionVar as itemVar>");
				}
			}
		}
		return rowIndex;
	}
	
	private Object getVal(String property, Object model) {
		return ModelUtil.getProperty(model, property);
	}
	private void replacePicture(Workbook wb, Sheet sheet, Object model) {
		List<ClientAnchor> picInfos = new ArrayList<ClientAnchor>();
		if (wb instanceof HSSFWorkbook) {
			HSSFSheet hssfSheet = (HSSFSheet)sheet;
			if (hssfSheet.getDrawingPatriarch() == null ) return;
			List<HSSFShape> oldPChildren = hssfSheet.getDrawingPatriarch().getChildren();
			if (oldPChildren == null || oldPChildren.isEmpty()) return ;
			for (HSSFShape shape : oldPChildren) {
				HSSFClientAnchor anchor = (HSSFClientAnchor) shape.getAnchor();
	            if (shape instanceof HSSFPicture) {
	            	// TODO: use shape.getFileName() as the picName?
	                picInfos.add(anchor);
				}
			}
			
			// clear all the old picture data
			hssfSheet.getDrawingPatriarch().clear();
		} else {
			Drawing drawing = sheet.createDrawingPatriarch();
			XSSFDrawing patriarch = (XSSFDrawing)drawing;
			List<XSSFShape> shapes = patriarch.getShapes();
			for (XSSFShape shape : shapes) {
				XSSFAnchor anchor = shape.getAnchor();
				if (anchor instanceof ClientAnchor) {
					picInfos.add((ClientAnchor)anchor);
				}
			}
			
			// TODO: remove all the old picture data
		}
		
		int picNameIndex = 0;
		for (ClientAnchor anchor : picInfos) {
			addPicture(wb, sheet, anchor, picNameIndex++, model);
		}
	}
	private int addPicture(Workbook wb, Sheet hssfSheet, ClientAnchor anchor, int picNameIndex, Object model) {
		String fileName = (String) getVal("picture_" + picNameIndex, model);
		int newPicIndex = -1;
		if (fileName != null) {
			// see txFileService.checkAndGetRealFilePath
			String fullFileName = config.getRealWebRootPath() + fileName;
			if (!new File(fullFileName).exists()) {
				fullFileName = config.getRealSitePath() + fileName;
			}
			try {
				byte[] pictureData = IOUtils.toByteArray(new FileInputStream(fullFileName));
				
				int picType = Workbook.PICTURE_TYPE_JPEG;
				String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
				if ("jpg".equals(extension) || "jpeg".equals(extension)) {
					picType = Workbook.PICTURE_TYPE_JPEG;
				} else if ("png".equals(extension)) {
					picType = Workbook.PICTURE_TYPE_PNG;
				}
				
				newPicIndex = wb.addPicture(pictureData, picType);
				Drawing patriarch = hssfSheet.createDrawingPatriarch();
				patriarch.createPicture(anchor, newPicIndex);
				anchor.setAnchorType(ClientAnchor.MOVE_DONT_RESIZE);
			} catch (IOException e) {
				logger.error("Failed to load picture file:" + fullFileName, e);
			}
		}
		return newPicIndex;
	}
	
	private int runMacroList(String collectionName, String propertyName, Object model, Sheet sheet,
			int rowIndex, int cellIndex, int replaceRowCount) {
		Object collection = getVal(collectionName, model);
		Collection coll;
		if (collection instanceof Collection) {
			coll = (Collection)collection;
		} else {
			coll = new ArrayList(1);	// add a blank row
			coll.add(new HashMap());
		}
		
		int row = rowIndex;
		Map<Integer, String> replaceStrs = new HashMap<Integer, String>();
		Map<Integer, Object> keepStrs = new HashMap<Integer, Object>();
		int maxCellNum = 0;
		Map<String, Object> content = new HashMap<String, Object>();
		if (model instanceof Map) {
			content.putAll((Map)model);
		} else {
			content.put("model", model);
		}
		
		int offset = coll.size() - 1 - replaceRowCount;
		if (coll.size() > 1 && offset > 0) {
			// empty rows
			List<Integer> zeroHeightNums = new ArrayList<Integer>();
			for (int i = 0, max = sheet.getLastRowNum(); i <= max; i++) {
				Row theRow = sheet.getRow(i);
				if (theRow != null && theRow.getZeroHeight()) {
					zeroHeightNums.add(i);
				}
			}
			// leave blank area for the new inserted rows
			sheet.shiftRows(rowIndex + 1, sheet.getLastRowNum(), offset, true, true);
			// move empty rows
			for (int i = 0, len = zeroHeightNums.size(); i < len; i++) {
				int index = zeroHeightNums.get(i);
				Row oldRow = sheet.getRow(index);
				oldRow.setZeroHeight(false);
				Row newRow = sheet.getRow(index + offset);
				if (newRow == null) {
					newRow = sheet.createRow(index + offset);
				}
				newRow.setZeroHeight(true);
			}
		}
		
		Row sourceRow = sheet.getRow(rowIndex);
		int i = -1;		// record index
		for (Object item : coll) {
			i++;
			content.put(propertyName, item);
			Row sheetRow;
			if (row == rowIndex) {
				sheetRow = sheet.getRow(row);
				maxCellNum = sheetRow.getLastCellNum();
			} else {
				if (i >= replaceRowCount) {		// replace the first n row
					sheetRow = sheet.createRow(row);
					sheetRow.setHeight(sourceRow.getHeight()); 
				} else {						// insert the new row
					sheetRow = sheet.getRow(row);
				}
			}
			
			for (int index = cellIndex; index <= maxCellNum; index++) {		// maxCellNum - 1?
				Cell cell = sheetRow.getCell(index);
				if (row == rowIndex) {		// replace & prepare the variable for the first line
					if (cell != null) {						
						if (cell.getCellType() == Cell.CELL_TYPE_STRING) {
							String val = cell.getStringCellValue();
							if (val.length() > 0) {
								boolean processed = tryToReplaceVar(content, cell, val);
								if (processed) {
									if (index == cellIndex) {
										// ignore the <#macro>
										if (val.startsWith("<#")) {
											int end = val.indexOf('>');
											val = val.substring(end + 1);
											cell.setCellValue(getElValue(content, val));
										}
									}
									replaceStrs.put(index, val);
								} else {
									keepStrs.put(index, val);
								}
							}
						} else {
							keepStrs.put(index, getCellValue(cell));
						}
					}
				} else {				// insert the new line according to the variable
					String str = replaceStrs.get(index);
					
					if (i >= replaceRowCount) {
						// copy cell
						cell = sheetRow.createCell(index);
					} else {
						// try to use the existed cell
						cell = sheetRow.getCell(index);
						if (cell == null) {
							cell = sheetRow.createCell(index);
						}
					}
					
					Cell sourceCell = sourceRow.getCell(index);
					if (sourceCell != null) {
						cell.setCellStyle(sourceCell.getCellStyle());
						if (sourceCell.getCellComment() != null) {  
							cell.setCellComment(sourceCell.getCellComment());  
						}  
						int cellType = sourceCell.getCellType();
						cell.setCellType(cellType); 
						if (cellType == Cell.CELL_TYPE_ERROR) {  
							cell.setCellErrorValue(sourceCell.getErrorCellValue());  
					    } else if (cellType == Cell.CELL_TYPE_FORMULA) {  
					    	cell.setCellFormula(sourceCell.getCellFormula());  
					    }
					}
					
					// set value
					if (str != null) {
						tryToReplaceVar(content, cell, str);
					} else {
						setCellValue(cell, keepStrs.get(index));
					}
				}
			}
			row++;
		}
		
		if (coll.size() > 1) {
			applyMergeStyle(sheet, rowIndex, coll.size());
		}
		return row - 1;
	}
	
	private void applyMergeStyle(Sheet sheet, int beginRow, int rowLen) {
		List<CellRangeAddress> oldRanges = new ArrayList<CellRangeAddress>();
		for (int i = 0, len = sheet.getNumMergedRegions(); i < len; i++) {
			CellRangeAddress oldRange = sheet.getMergedRegion(i);
			if (oldRange.getFirstRow() == beginRow) {
				oldRanges.add(oldRange);
			}
		}
		
		// 拷贝合并的单元格. 原理:复制当前合并单元格后,原位置的格式会移动到新位置,需在原位置生成旧格式
		for (int i = 1; i < rowLen; i++) {
			for (CellRangeAddress oldRange : oldRanges) {
				CellRangeAddress newRange = new CellRangeAddress(
						oldRange.getFirstRow() + i, oldRange.getLastRow() + i,
						oldRange.getFirstColumn(), oldRange.getLastColumn());
				sheet.addMergedRegion(newRange);
			}
		}
	}

	private boolean tryToReplaceVar(Object model, Cell cell, String val) {
		String newVal = getElValue(model, val);
		if (newVal != val) {
			setCellValue(cell, newVal);
			return true;
		} else {
			return false;
		}
	}
	
	// most case is no variable or one variable
	private String getElValue(Object model, String val) {
		int begin = val.indexOf("${");
		if (begin >=0 && val.indexOf('}') < 0){
			throw new ApplicationException("error excel format: no '}' found");
		}
		if (begin == -1) {		// no variable
			return val;
		}
		
		// for fast speed, use 2 variable to store the new replaced value
		String replaceVal = null;
		StringBuilder sbReplaceVal = null;
		int sbIndex = 0;
		int end = -1;
		while (begin != -1) {
			if (begin >= 0) {
				end = val.indexOf('}', begin + 2);
			}
			
			if (end > begin) {
				String property = val.substring(begin + 2, end);
				Object newVal = getVal(property, model);
				String strNewVal;
				if (newVal == null || newVal instanceof String) {
					strNewVal = (String)newVal;
				} else {
					strNewVal = ognlUtil.convertValue(newVal, String.class, property);
				}
				
				// one variable, quick check
				if (begin == 0 && end == val.length() - 1) {
					replaceVal = strNewVal;
					break;
				}
				
				if (sbReplaceVal == null) {
					sbReplaceVal = new StringBuilder(val.length() + 10);
				}
				if (begin > sbIndex) {
					sbReplaceVal.append(val.substring(sbIndex, begin));
				}
				if (strNewVal != null) {
					sbReplaceVal.append(strNewVal);
				}
				sbIndex = end + 1;
			} else {
				// wrong grammar, ignore it
			}
			
			begin = val.indexOf("${", end + 1);
		}
		if (sbReplaceVal != null && sbIndex < val.length()) {
			sbReplaceVal.append(val.substring(sbIndex));
		}
		
		return replaceVal != null || sbReplaceVal == null ? replaceVal : sbReplaceVal.toString();
	}
	
	/*
	@Rest(type="report")
	public <T> Result printPdf(@GenericType T model, String path) {
		ActionContext actionContext =  ActionContext.getContext();
    	HttpServletRequest request = (HttpServletRequest) actionContext.get(StrutsStatics.HTTP_REQUEST);
		String userId = safeManager.getLoginUser(request).getLoginId();
		boolean pass = safeManager.checkPage(userId, path, null);
		if (!pass) {
			return null;
		}
		
		// 1. execute template to get the page content
		String content = getInvocationUtil().invokeTpl(path);
		
		try {
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
	        
			// 2. run js to get the real html content (TODO: optimize this code)
			String parsedContent;
			if (content.indexOf("</script>") > 0) { 
				parsedContent = javaScriptShell.runHTML(content);
			} else {
				parsedContent = content;
			}
			
			StringBuilder headStyle = new StringBuilder();		// TODO: use htmlCleaner?
			StringBuilder sbParsedContent = new StringBuilder();
			
			int styleIndex = parsedContent.indexOf("<style type=\"text/css\">");
			if (styleIndex >= 0) {
				int lasEndIndex = -1;
				while (styleIndex >= 0) {
					int styleEndIndex = parsedContent.indexOf("</style>", styleIndex);
					if (styleEndIndex > styleIndex) {
						sbParsedContent.append(parsedContent.substring(lasEndIndex + 1, styleIndex));
						lasEndIndex = styleEndIndex + 8;
						headStyle.append(parsedContent.substring(styleIndex, lasEndIndex));
						// continue to find
						styleIndex = parsedContent.indexOf("<style type=\"text/css\">", lasEndIndex);
					} else {
						styleIndex = -1;
					}
				}
				sbParsedContent.append(parsedContent.substring(lasEndIndex + 1));
				parsedContent = sbParsedContent.toString();
			}
			
			String cssPrefix = "<html><head><link href='styles/jquery-ui-1.8.18.custom.css' type='text/css' rel='Stylesheet'/>"
				+ "<link href='styles/jtaglib.css' type='text/css' rel='Stylesheet'/>"
				+ "<link href='styles/jespirit.css' type='text/css' rel='Stylesheet'/>"
				+ headStyle
				+ "</head><body class='printView'>";
			content = cssPrefix  + parsedContent.replace('&', ' ') + "</body></html>";		//TODO:
			InputStream htmlreader = new ByteArrayInputStream(content.getBytes("UTF-8"));
	        DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
	        Document document = builder.parse(htmlreader);

	        // 3. flying sauser, convert html to pdf
	        ITextRenderer renderer = new ITextRenderer();
	        // Chinese font
	        ITextFontResolver fontResolver = renderer.getFontResolver();
	        String fontFile = this.getClass().getResource("/SIMSUN.TTC").getFile();
	        fontResolver.addFont(fontFile, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
	        
	        // relative path
	        String baseUrl = "file:/" + config.getWebRootPath();
	        if (baseUrl.charAt(baseUrl.length() - 1) != File.separatorChar) {
	        	baseUrl += File.separatorChar;
	        }
	        renderer.getSharedContext().setBaseURL(baseUrl);
	        renderer.setDocument(document, baseUrl);
	        
	        renderer.layout();
	        renderer.createPDF(baos);
	        baos.close();
	        
	        // 4. output pdf
	        byte[] reportContext = baos.toByteArray();
	        int fileEndIndex = path.indexOf('?');
	        if (fileEndIndex < 0) {
	        	fileEndIndex = path.length();
	        }
	        int fileIndex = path.lastIndexOf('/', fileEndIndex) + 1;
	        fileEndIndex = path.indexOf('.', fileIndex);
	        if (fileEndIndex < 0) {
	        	fileEndIndex = path.length();
	        }
	        String fileName = path.substring(fileIndex, fileEndIndex);
	        outputFile(fileName, true, reportContext);
		} catch (Exception ex) {
			logger.error("generate report", ex);
		}
		return null;
	}*/
	
	@Rest(type="update")
	public Result viewFile(String name) throws Exception {
		// read file and output it the outputstream
		String rootDir = config.getRealWebRootPath();
		File realPathFile = new File(rootDir + name);
		if (!realPathFile.exists()) {
			rootDir = config.getRealSitePath();
			realPathFile = new File(rootDir + name);
			
			if (!realPathFile.exists()) {
				if (logger.isDebugEnabled()) {
					logger.debug("Wrong file name is " + realPathFile.getName());
				}
				throw new ApplicationException("errors.wrongFile");
			}
		}
		
		if (!StringUtil.startsWithSkipFileSeparator(realPathFile.getPath(), rootDir)) {		// security check
			logger.error("Found wrong path, the input parameter is " + name 
					+ ", from ip " + ServletActionContext.getRequest().getRemoteAddr());
			return null;
		}
		
		
		HttpServletResponse response = ServletActionContext.getResponse();
		// format the name, see tagUtil.formatFileName
		int begin = name.lastIndexOf('/') + 1;
		int index1 = name.indexOf('.', begin);
		if (index1 > 0) {
			int index2 = name.indexOf('.', index1 + 1);
			if (index2 > 0) {
				begin = index1 + 1;
			}
		}
		
		byte[] context = FileUtils.readFileToByteArray(realPathFile);
		Object model = null;		// FIXME: how to know the file is encrypted?
		if (model instanceof EncryptedFile) {	// see TxFile.commit
			try {
				// TODO: each file has their own encrypted seed?
				String SEED = "0966584141573219732317534";
				BigInteger bi_text = new BigInteger(context);
				BigInteger bi_r0 = new BigInteger(SEED);
				BigInteger bi_r1 = bi_r0.xor(bi_text);
				context = bi_r1.toByteArray();
			} catch (Exception ex) {
				logger.error("Failed to encrypt the file", ex);
			}
		}
		
        String fileName = new String(name.substring(begin).getBytes("UTF-8"), "ISO8859_1");
        
        response.reset();
        String contentType = getContentTypeFor(fileName);
        response.setContentType(contentType);
        response.addHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\"");
		
		OutputStream outputStream = response.getOutputStream();
		outputStream.write(context, 0, context.length);
		outputStream.close();
		return null;
	}
	
	@Rest(type="update")
	public String upload(@GenericType Object model) {
		List<TxFile> uploadFiles = (List<TxFile>) ActionContext.getContext().getValueStack().findValue(FrameConstant.STACK_UPLOADFILES);
		if (uploadFiles != null && uploadFiles.size() > 0) {
			getTxFileService().prepareTxFile(uploadFiles);
			
			StringBuilder sb = new StringBuilder();
			for (TxFile txFile : uploadFiles) {
				if (sb.length() > 0) {
					sb.append(',');
				}
				sb.append(txFile.getTo().getName());

				if (logger.isDebugEnabled()) {
					logger.debug("upload txFile:" + txFile.getFullFilePropertyName() + "," + txFile.getDestPicFileName() + "," + txFile.getFrom().getName());
				}
				// 第二次批量提交时， 借助FrameConstant.PARAM_COMMIT_FILENAME中的文件名，统一处理
			}
			return sb.toString();
		} else {
			return "";
		}
	}
}